package com.google.code.mochaccino.framework.crypto.x509;

/*
 * Copyright 2012 Claude Houle claude.houle@gmail.com
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import com.google.code.mochaccino.framework.crypto.EncryptionException;
import com.google.code.mochaccino.framework.crypto.EncryptionProvider;
import com.google.common.base.Preconditions;
import java.io.IOException;
import java.math.BigInteger;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.Provider;
import java.security.PublicKey;
import java.security.SecureRandom;
import java.security.Security;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.security.interfaces.RSAPrivateKey;
import java.security.interfaces.RSAPublicKey;
import java.util.Calendar;
import org.bouncycastle.asn1.ASN1Object;
import org.bouncycastle.asn1.ASN1Sequence;
import org.bouncycastle.asn1.x500.X500Name;
import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo;
import org.bouncycastle.cert.X509CertificateHolder;
import org.bouncycastle.cert.X509v1CertificateBuilder;
import org.bouncycastle.cert.X509v3CertificateBuilder;
import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter;
import org.bouncycastle.operator.ContentSigner;
import org.bouncycastle.operator.OperatorCreationException;
import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder;
import org.bouncycastle.operator.jcajce.JceAsymmetricKeyWrapper;

/** The X509 Certificate Generate is made to generate SSL RSA Certificate in large quantities. */
public final class X509CertificateGenerator {
	private static final String DEFAULT_CERTIFICATE_ALGO = "RSA";
	private static final String DEFAULT_SIGNATURE_ALGO   = "SHA512WithRSAEncryption";
	private static final int    DEFAULT_KEY_PAIR_SIZE    = 2048;

	/** Activation Date */
	private Calendar activationDate = null;

	/** Expirate Date */
	private Calendar expirationDate = null;

	/** KeyPair Size */
	private Integer keyPairSize = DEFAULT_KEY_PAIR_SIZE;

	/** Signer Builder */
	private JcaContentSignerBuilder signerBuilder;

	/** KeyPair Generator */
	private KeyPairGenerator keyPairGenerator = null;

	/** Issuer */
	private X500Name issuer = null;

	/** The Subject */
	private X500Name subject = null;

	/** Provider */
	private Provider provider;

	/** Constructor */
	public X509CertificateGenerator( String issuerAndSubject ) throws EncryptionException {
		this( issuerAndSubject, issuerAndSubject );
	}

	/** Constructor */
	public X509CertificateGenerator( String issuer, String subject ) throws EncryptionException {
		this( DEFAULT_CERTIFICATE_ALGO, EncryptionProvider.getBouncyCastleName(), issuer, subject, Calendar.getInstance(), getNextYear(), 2048, DEFAULT_SIGNATURE_ALGO );
	}

	/** Constructor */
	public X509CertificateGenerator( String algo, String provider, String issuer, String subject, Calendar activationDate, Calendar expirationDate, Integer keyPairSize, String signatureAlgo ) throws EncryptionException {
		super();
		Preconditions.checkNotNull( algo, "Algorithm cannot be null" );
		this.provider = Preconditions.checkNotNull( Security.getProvider( provider ), "Unknown security provider" );
		this.issuer = new X500Name( Preconditions.checkNotNull( issuer, "Issuer cannot be null" ) );
		this.subject = new X500Name( Preconditions.checkNotNull( subject, "Subject cannot be null" ) );
		this.activationDate = Preconditions.checkNotNull( activationDate, "Activation Date cannot be null" );
		this.expirationDate = Preconditions.checkNotNull( expirationDate, "Expiration Date cannot be null" );
		this.keyPairSize = Preconditions.checkNotNull( keyPairSize, "Key-Pair Size cannot be null" );

		// Initialize Key Pair Generator
		try {
			keyPairGenerator = KeyPairGenerator.getInstance( algo, this.provider );
			keyPairGenerator.initialize( this.keyPairSize, new SecureRandom() );
		} catch ( NoSuchAlgorithmException e ) {
			throw new EncryptionException( e );
		}

		signerBuilder = new JcaContentSignerBuilder( signatureAlgo );
		signerBuilder.setProvider( this.provider );
	}

	private static Calendar getNextYear() {
		Calendar cal = Calendar.getInstance();
		cal.add( Calendar.YEAR, 1 );
		return cal;
	}

	/** Generate Issuer */
	public static String generateIssuer( String commonName, String organizationUnit, String organization, String city, String state, String country2LetterIsoCode ) {
		StringBuffer buf = new StringBuffer();
		buf.append( "CN=" );
		buf.append( Preconditions.checkNotNull( commonName, "Common Name cannot be null!" ) );
		if ( organizationUnit != null ) {
			buf.append( ", OU=" );
			buf.append( organizationUnit );
		}

		if ( organization != null ) {
			buf.append( ", O=" );
			buf.append( organization );
		}

		if ( city != null ) {
			buf.append( ", L=" );
			buf.append( city );
		}

		if ( state != null ) {
			buf.append( ", ST=" );
			buf.append( state );
		}

		if ( country2LetterIsoCode != null ) {
			Preconditions.checkArgument( country2LetterIsoCode.length() == 2, "Country ISO Code must be at least 2 letter in length." );
			buf.append( ", C=" );
			buf.append( country2LetterIsoCode );
		}
		return buf.toString();
	}

	/** Generate V1 */
	public X509CertificateInfo generateV1() throws EncryptionException {
		return generateV1( System.currentTimeMillis() );
	}

	/** Generate V1 */
	public X509CertificateInfo generateV1( long serialNumber ) throws EncryptionException {
		// Create the keys
		KeyPair keyPair = keyPairGenerator.generateKeyPair();
		PublicKey publicKey = keyPair.getPublic();
		PrivateKey privateKey = keyPair.getPrivate();
		return generateV1( serialNumber, (RSAPublicKey) publicKey, (RSAPrivateKey) privateKey );
	}

	/** Generate V1 */
	private X509CertificateInfo generateV1( long serialNumber, RSAPublicKey publicKey, RSAPrivateKey privateKey ) throws EncryptionException {
		try {
			JceAsymmetricKeyWrapper wrapper = new JceAsymmetricKeyWrapper( publicKey );
			wrapper.setProvider( this.provider );

			SubjectPublicKeyInfo keyInfo = new SubjectPublicKeyInfo( (ASN1Sequence) ASN1Object.fromByteArray( publicKey.getEncoded() ) );
			X509v1CertificateBuilder certificateGenerator = new X509v1CertificateBuilder( issuer, BigInteger.valueOf( serialNumber ), activationDate.getTime(), expirationDate.getTime(), subject, keyInfo );

			JcaX509CertificateConverter converter = new JcaX509CertificateConverter();
			converter.setProvider( this.provider );
			ContentSigner signer = signerBuilder.build( privateKey );
			X509CertificateHolder holder = certificateGenerator.build( signer );
			X509Certificate certificate = converter.getCertificate( holder );

			return new X509CertificateInfo( certificate, privateKey, publicKey );
		} catch ( OperatorCreationException e ) {
			throw new EncryptionException( e );
		} catch ( CertificateException e ) {
			throw new EncryptionException( e );
		} catch ( IOException e ) {
			throw new EncryptionException( e );
		}
	}

	/** Generate V3 */
	public X509CertificateInfo generateV3() throws EncryptionException {
		return generateV3( System.currentTimeMillis() );
	}

	/** Generate V3 */
	public X509CertificateInfo generateV3( long serialNumber ) throws EncryptionException {
		// Create the keys
		KeyPair keyPair = keyPairGenerator.generateKeyPair();
		RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic();
		RSAPrivateKey privateKey = (RSAPrivateKey) keyPair.getPrivate();
		return generateV3( serialNumber, publicKey, privateKey );
	}

	private X509CertificateInfo generateV3( long serialNumber, RSAPublicKey publicKey, RSAPrivateKey privateKey ) throws EncryptionException {
		try {
			JceAsymmetricKeyWrapper wrapper = new JceAsymmetricKeyWrapper( publicKey );
			wrapper.setProvider( EncryptionProvider.getBouncyCastleProvider() );

			SubjectPublicKeyInfo keyInfo = new SubjectPublicKeyInfo( (ASN1Sequence) ASN1Object.fromByteArray( publicKey.getEncoded() ) );
			X509v3CertificateBuilder certificateGenerator = new X509v3CertificateBuilder( issuer, BigInteger.valueOf( serialNumber ), activationDate.getTime(), expirationDate.getTime(), subject, keyInfo );

			JcaX509CertificateConverter converter = new JcaX509CertificateConverter();
			converter.setProvider( this.provider );
			ContentSigner signer = signerBuilder.build( privateKey );
			X509CertificateHolder holder = certificateGenerator.build( signer );
			X509Certificate certificate = converter.getCertificate( holder );
			return new X509CertificateInfo( certificate, privateKey, publicKey );
		} catch ( OperatorCreationException e ) {
			throw new EncryptionException( e );
		} catch ( CertificateException e ) {
			throw new EncryptionException( e );
		} catch ( IOException e ) {
			throw new EncryptionException( e );
		}
	}
}
